//
// Created by 匡涂上青 on 2021/8/12.
//

#include "test.h"

#include <kgl.h>

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <stb_image.h>
#include <imgui.h>
#include <backends/imgui_impl_glfw.h>
#include <backends/imgui_impl_opengl3.h>

#include <glm/gtc/type_ptr.hpp>

#include "FPSCamera.h"

constexpr float pi = glm::pi<float>();

int ch4_shadow_main() {
    kgl::Log::Init();
    auto win = &kgl::singleton<kgl::Application>::getInstance()->GetWindow();
    glm::ivec2 winSize = win->GetSize();

    // create shader
    auto shader_light = kgl::MakeRef<kgl::Shader>("../kgl-data/shader/ch4_light.glsl");
    auto shader_shadow = kgl::MakeRef<kgl::Shader>("../kgl-data/shader/ch4_shadowMap.glsl");

    // create quad
    float vert_quad[] = {
            // positions       // normal         // texCoords
            -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
            -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
            1.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,

            -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
            1.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
            1.0f, 1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f
    };
    // vao_quad
    unsigned int vao_quad;
    glGenVertexArrays(1, &vao_quad);
    glBindVertexArray(vao_quad);
    // vbo_quad
    unsigned int vbo_quad;
    glGenBuffers(1, &vbo_quad);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_quad);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vert_quad), vert_quad, GL_STATIC_DRAW);
    // pos
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *) nullptr);
    glEnableVertexAttribArray(0);
    // normal
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *) (3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    // texCoords
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *) (6 * sizeof(float)));
    glEnableVertexAttribArray(2);

    // create box
    float vert_box[] = {
            // position          //normal           // texCoords
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
            0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
            0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
            0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
            -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,

            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
            0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
            0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
            -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,

            -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
            -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
            -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
            -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,

            0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
            0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
            0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
            0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,

            -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
            0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
            0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
            0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
            -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,

            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
            0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
            0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
            -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
    };
    // vao_box
    unsigned int vao_box;
    glGenVertexArrays(1, &vao_box);
    glBindVertexArray(vao_box);
    // vbo_box
    unsigned int vbo_box;
    glGenBuffers(1, &vbo_box);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_box);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vert_box), vert_box, GL_STATIC_DRAW);
    // pos
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *) nullptr);
    glEnableVertexAttribArray(0);
    // normal
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *) (3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    // texCoords
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *) (6 * sizeof(float)));
    glEnableVertexAttribArray(2);

    // textures and material
    int w, h, ch;
    unsigned int tex_diffuse;
    glGenTextures(1, &tex_diffuse);
    glBindTexture(GL_TEXTURE_2D, tex_diffuse);
    // set texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // load texture
    auto stb_deleter = [](unsigned char *data) { stbi_image_free(data); };
    // diffuse texture
    kgl::Scope<unsigned char, decltype(stb_deleter)> image_diffuse(
            stbi_load("../kgl-data/texture/brickwall.jpg", &w, &h, &ch, 0), stb_deleter);
    if (nullptr != image_diffuse) {
        if (ch == 3)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, image_diffuse.get());
        else if (ch == 4)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_diffuse.get());
        else if (ch == 1)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, image_diffuse.get());
        // gen mipmap
        glGenerateMipmap(GL_TEXTURE_2D);
    } else {
        kgl::Log::ErrorCore("Failed to load diffuse texture");
    }
    shader_light->Use();
    shader_light->SetSampler2D("_material.diffuse", 0);
    // specular texture
    unsigned int tex_specular;
    glGenTextures(1, &tex_specular);
    glBindTexture(GL_TEXTURE_2D, tex_specular);
    // set texture wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // set texture filtering parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    kgl::Scope<unsigned char, decltype(stb_deleter)> image_specular(
            stbi_load("../kgl-data/texture/brickwall_spec.png", &w, &h, &ch, 0), stb_deleter);
    if (nullptr != image_specular) {
        if (ch == 3)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, image_specular.get());
        else if (ch == 4)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_specular.get());
        else if (ch == 1)
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, image_specular.get());
        // gen mipmap
        glGenerateMipmap(GL_TEXTURE_2D);
    } else {
        kgl::Log::ErrorCore("Failed to load specular texture");
    }
    shader_light->SetSampler2D("_material.specular", 1);

    shader_light->SetInt("_material.shininess", 32);

    // light meta data
    // direct light
    shader_light->SetInt("_direct_light_nums", 1);
    int index = 0;
    std::string lightStr = "_direct_lights[" + std::to_string(index) + "]";
    shader_light->SetFloat(lightStr + ".intensity", 1.0);
    shader_light->SetRGB(lightStr + ".ambient", {0.1f, 0.1f, 0.1f});
    shader_light->SetRGB(lightStr + ".diffuse", {1.0f, 1.0f, 1.0f});
    shader_light->SetRGB(lightStr + ".specular", {1.0f, 1.0f, 1.0f});
    glm::vec3 direction(1.0f, 1.0f, 1.0f);
    float light_area = 1.0f;
    shader_light->SetVector3(lightStr + ".direction", direction);
    // light camera
    glm::ivec2 shadow_map_size{2048, 2048};
    shader_light->SetVector2("_shadow_map_size", shadow_map_size);

    // transform
    glm::vec3 trans1_rotate(0.0f);
    kgl::Transform trans1(glm::vec3(20, -4.8f, -40.0f), trans1_rotate, glm::vec3{10.0f, 50.0f, 10.0f});

    kgl::Transform trans2(glm::vec3(0, -30.0f, -60.0f),
                          glm::vec3(glm::radians(90.0f), 0, 0), glm::vec3(50.0f));

    auto trans_ca = kgl::MakeRef<kgl::Transform>(glm::vec3(0, 0, 500.0f),
                                                 glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(1.0f));

    // frame buffer
    unsigned int framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    // create a color attachment texture
    unsigned int tex_shadowMap;
    glGenTextures(1, &tex_shadowMap);
    glBindTexture(GL_TEXTURE_2D, tex_shadowMap);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, shadow_map_size.x, shadow_map_size.y, 0, GL_RGB,
                 GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex_shadowMap, 0);
//    glDrawBuffer(GL_NONE);
//    glReadBuffer(GL_NONE);
    shader_light->SetSampler2D("_shadow_map", 2);
    // create a renderbuffer object for depth and stencil attachment (we won't be sampling these)
    unsigned int rbo;
    glGenRenderbuffers(1, &rbo);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, shadow_map_size.x,
                          shadow_map_size.y); // use a single renderbuffer object for both a depth AND stencil buffer.
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
                              rbo); // now actually attach it
    // now that we actually created the framebuffer and added all attachments we want to check if it is actually complete now
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        kgl::Log::Error("FRAMEBUFFER:: Framebuffer is not complete!");
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // script
    auto fpsCamera = new FPSCamera(trans_ca,
                                   kgl::MakeRef<kgl::PerspectiveCamera>(
                                           *trans_ca, glm::radians(60.0f),
                                           (float) winSize.x / (float) winSize.y, 0.01f, 1000.0f));

    ScriptManager scriptManager;
    scriptManager.add_script(fpsCamera);

    glm::vec4 clearColor = {0.24f, 0.24f, 0.24f, 1.0f};

    // render init
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    scriptManager.Setup();

    ImGui::CreateContext();
    ImGuiIO &io = ImGui::GetIO();
    (void) io;
    ImGui_ImplGlfw_InitForOpenGL(win->GetGLFWwindow(), true);
    ImGui_ImplOpenGL3_Init("#version 330 core");
    ImGui::StyleColorsDark();
    bool show_demo_window = true;

    while (kgl::singleton<kgl::Application>::getInstance()->isOpen()) {

        scriptManager.Update();

        // ImGui Update

        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();
        ImGui::Begin("Propertis Window", &show_demo_window);
        ImGui::DragFloat3("position", glm::value_ptr(trans1.position), 0.2f, -1000.0f, 1000.0f, "%.2f", 1.0f);
        ImGui::DragFloat3("rotation", glm::value_ptr(trans1_rotate), 0.02f, -pi, pi, "%.3f", 1.0f);
        ImGui::DragFloat3("scale", glm::value_ptr(trans1.scale), 0.02f, -400.0f, 400.0f, "%.3f", 1.0f);
        ImGui::DragFloat3("light direction", glm::value_ptr(direction), 0.02f, -pi, pi, "%.3f", 1.0f);
        ImGui::DragFloat("move speed", &fpsCamera->moveSpeed, 0.01f, 0.01f, 1.0f, "%.2f", 1.0f);
        ImGui::DragFloat("light erea", &light_area, 0.02f, 0.0f, 10.0f, "%.2f", 1.0f);
        ImGui::End();
        ImGui::Render();

        auto sm_proj = glm::ortho(-200.0f, 200.0f, -200.0f, 200.0f, -1.0f, 400.0f);
        auto sm_view = glm::lookAt(direction * 10.0f, {0, 0, 0.0f},
                                   glm::vec3{0, 1, 0});
        auto lvp_mat = sm_proj * sm_view;

        trans1.rotation = glm::quat(trans1_rotate);

        // render to framebuffer
        glViewport(0, 0, shadow_map_size.x, shadow_map_size.y);
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        glEnable(GL_DEPTH_TEST); // enable depth testing (is disabled for rendering screen-space quad)
        glCullFace(GL_FRONT);
        glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        shader_shadow->Use();
        shader_shadow->SetMatrix4("_light_space_matrix", lvp_mat);
        shader_shadow->SetMatrix4("_model", trans1.get_trans_mat());

        glBindVertexArray(vao_box);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        shader_shadow->Use();
        shader_shadow->SetMatrix4("_model", trans2.get_trans_mat());

        glBindVertexArray(vao_quad);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        // render to default screen buffer
        winSize = win->GetSize();
        glViewport(0, 0, winSize.x, winSize.y);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glCullFace(GL_BACK);
        glClearColor(0.3f, 0.5f, 0.5f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, tex_diffuse);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, tex_specular);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, tex_shadowMap);

        shader_light->Use();
        shader_light->SetMatrix4("_model", trans1.get_trans_mat());
        shader_light->SetMatrix4("_view", fpsCamera->get_camera()->get_view_mat());
        shader_light->SetMatrix4("_projection", fpsCamera->get_camera()->get_projection_mat());
        shader_light->SetVector3("_eye_pos", trans_ca->position);
        shader_light->SetVector3(lightStr + ".direction", direction);
        shader_light->SetMatrix4("_light_space_matrix", lvp_mat);
        shader_light->SetFloat("_light_area", light_area);

        glBindVertexArray(vao_box);
        glDrawArrays(GL_TRIANGLES, 0, 36);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, tex_diffuse);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, tex_specular);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, tex_shadowMap);

        shader_light->Use();
        shader_light->SetMatrix4("_model", trans2.get_trans_mat());

        glBindVertexArray(vao_quad);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
        win->Display();
    }

    scriptManager.Destroy();

    glDeleteVertexArrays(1, &vao_box);
    glDeleteVertexArrays(1, &vao_quad);
    glDeleteBuffers(1, &vbo_box);
    glDeleteBuffers(1, &vbo_quad);

    return 0;
}